<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
        <meta
            name="viewport"
            content="width=device-width, minimal-ui, viewport-fit=cover, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
        />
        <link rel="icon" type="image/png" href="assets/favicon.png" />

        <title>OGL • GPGPU Particles (General-Purpose computing on Graphics Processing Units)</title>
        <link href="assets/main.css" rel="stylesheet" />
    </head>
    <body>
        <div class="Info">GPGPU Particles (General-Purpose computing on Graphics Processing Units)</div>
        <script type="module">
            import { Renderer, Camera, Geometry, Program, Texture, Mesh, Vec2, GPGPU } from '../src/index.mjs';

            const vertex = /* glsl */ `
                attribute vec2 coords;
                attribute vec4 random;

                uniform float uTime;
                uniform sampler2D tPosition;
                uniform sampler2D tVelocity;

                varying vec4 vRandom;
                varying vec4 vVelocity;

                void main() {
                    vRandom = random;

                    // Get position from texture, rather than attribute
                    vec4 position = texture2D(tPosition, coords);
                    vVelocity = texture2D(tVelocity, coords);
                    
                    // Add some subtle random oscillating so it never fully stops
                    position.xy += sin(vec2(uTime) * vRandom.wy + vRandom.xz * 6.28) * vRandom.zy * 0.1;

                    gl_Position = vec4(position.xy, 0, 1);
                    gl_PointSize = mix(2.0, 15.0, vRandom.x);

                    // Make bigger while moving
                    gl_PointSize *= 1.0 + min(1.0, length(vVelocity.xy));

                }
            `;

            const fragment = /* glsl */ `
                precision highp float;

                varying vec4 vRandom;
                varying vec4 vVelocity;

                void main() {

                    // Circle shape
                    if (step(0.5, length(gl_PointCoord.xy - 0.5)) > 0.0) discard;

                    // Random colour
                    vec3 color = vec3(vRandom.zy, 1.0) * mix(0.7, 2.0, vRandom.w);

                    // Fade to white when not moving, with an ease off curve
                    gl_FragColor.rgb = mix(vec3(1), color, 1.0 - pow(1.0 - smoothstep(0.0, 0.7, length(vVelocity.xy)), 2.0));

                    gl_FragColor.a = 1.0;
                }
            `;

            const positionFragment = /* glsl */ `
                precision highp float;

                uniform float uTime;
                uniform sampler2D tVelocity;

                // Default texture uniform for GPGPU pass is 'tMap'.
                // Can use the textureUniform parameter to update.
                uniform sampler2D tMap;

                varying vec2 vUv;

                void main() {
                    vec4 position = texture2D(tMap, vUv);
                    vec4 velocity = texture2D(tVelocity, vUv);

                    position.xy += velocity.xy * 0.01;
                                    
                    // Keep in bounds
                    vec2 limits = vec2(1);
                    position.xy += (1.0 - step(-limits.xy, position.xy)) * limits.xy * 2.0;
                    position.xy -= step(limits.xy, position.xy) * limits.xy * 2.0;

                    gl_FragColor = position;
                }
            `;

            const velocityFragment = /* glsl */ `
                precision highp float;

                uniform float uTime;
                uniform sampler2D tPosition;
                uniform sampler2D tMap;
                uniform vec2 uMouse;

                varying vec2 vUv;

                void main() {
                    vec4 position = texture2D(tPosition, vUv);
                    vec4 velocity = texture2D(tMap, vUv);

                    // Repulsion from mouse
                    vec2 toMouse = position.xy - uMouse;
                    float strength = smoothstep(0.3, 0.0, length(toMouse));
                    velocity.xy += strength * normalize(toMouse) * 0.5;

                    // Friction
                    velocity.xy *= 0.98;

                    gl_FragColor = velocity;
                }
            `;

            {
                const renderer = new Renderer({ dpr: 2 });
                const gl = renderer.gl;
                document.body.appendChild(gl.canvas);
                gl.clearColor(1, 1, 1, 1);

                const camera = new Camera(gl, { fov: 45 });
                camera.position.set(0, 0, 5);

                function resize() {
                    renderer.setSize(window.innerWidth, window.innerHeight);
                    camera.perspective({ aspect: gl.canvas.width / gl.canvas.height });
                }
                window.addEventListener('resize', resize, false);
                resize();

                // Common uniforms
                const time = { value: 0 };
                const mouse = { value: new Vec2() };

                // The number of particles will determine how large the GPGPU textures are,
                // and therefore how expensive the GPU calculations will be.
                // Below I'm using 65536 to use every pixel of a 256x256 texture. If I used one more (65537),
                // it would need to use a 512x512 texture - the GPU would then perform calculations for each pixel,
                // meaning that nearly 3/4 of the texture (196607 pixels) would be redundant.
                const numParticles = 65536;

                // Create the initial data arrays for position and velocity. 4 values for RGBA channels in texture.
                const initialPositionData = new Float32Array(numParticles * 4);
                const initialVelocityData = new Float32Array(numParticles * 4);

                // Random to be used as regular static attribute
                const random = new Float32Array(numParticles * 4);
                for (let i = 0; i < numParticles; i++) {
                    initialPositionData.set(
                        [
                            (Math.random() - 0.5) * 2.0,
                            (Math.random() - 0.5) * 2.0,
                            0, // the Green and Alpha channels go unused in this example, however I set
                            1, // unused Alpha to 1 so that texture is visible in WebGL debuggers
                        ],
                        i * 4
                    );
                    initialVelocityData.set([0, 0, 0, 1], i * 4);
                    random.set([Math.random(), Math.random(), Math.random(), Math.random()], i * 4);
                }

                // Initialise the GPGPU classes, creating the FBOs and corresponding texture coordinates
                const position = new GPGPU(gl, { data: initialPositionData });
                const velocity = new GPGPU(gl, { data: initialVelocityData });

                // Add the simulation shaders as passes to each GPGPU class
                position.addPass({
                    fragment: positionFragment,
                    uniforms: {
                        uTime: time,
                        tVelocity: velocity.uniform,
                    },
                });
                velocity.addPass({
                    fragment: velocityFragment,
                    uniforms: {
                        uTime: time,
                        uMouse: mouse,
                        tPosition: position.uniform,
                    },
                });

                // Now we can create our geometry, using the coordinates from above.
                // We don't use the velocity or position data as attributes,
                // instead we will get this from the FBO textures in the shader.
                const geometry = new Geometry(gl, {
                    random: { size: 4, data: random },

                    // Could use either position or velocity coords, as they are the same
                    coords: { size: 2, data: position.coords },
                });

                const program = new Program(gl, {
                    vertex,
                    fragment,
                    uniforms: {
                        uTime: time,
                        tPosition: position.uniform,
                        tVelocity: velocity.uniform,
                    },
                });

                const points = new Mesh(gl, { geometry, program, mode: gl.POINTS });

                // Add handlers to get mouse position
                const isTouchCapable = 'ontouchstart' in window;
                if (isTouchCapable) {
                    window.addEventListener('touchstart', updateMouse, false);
                    window.addEventListener('touchmove', updateMouse, false);
                } else {
                    window.addEventListener('mousemove', updateMouse, false);
                }

                function updateMouse(e) {
                    if (e.changedTouches && e.changedTouches.length) {
                        e.x = e.changedTouches[0].pageX;
                        e.y = e.changedTouches[0].pageY;
                    }
                    if (e.x === undefined) {
                        e.x = e.pageX;
                        e.y = e.pageY;
                    }

                    // Get mouse value in -1 to 1 range, with y flipped
                    mouse.value.set((e.x / gl.renderer.width) * 2 - 1, (1.0 - e.y / gl.renderer.height) * 2 - 1);
                }

                requestAnimationFrame(update);
                function update(t) {
                    requestAnimationFrame(update);

                    time.value = t * 0.001;

                    // Update the GPGPU classes
                    velocity.render();
                    position.render();

                    renderer.render({ scene: points, camera });
                }
            }
        </script>
    </body>
</html>
